﻿/*!
 * Copyright (c), 上海沃恩信息科技有限公司.
 * All rights reserved.
 * Licensed under BSD (https://www.pittypat.work/bsd.html).
 */

using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.WebSockets;
using System.Threading.Tasks;

namespace Pittypat.Web
{
    /// <summary>
    /// 向客户端发送的流，流要支持读取操作，并且可计算长度。
    /// </summary>
    /// <remarks>
    /// 可用于在功能的 Api 中作为返回值向客户端发送二进制流数据。如果流的长度无法事先计算，可以使用 <see cref="AsyncStream"/>。
    /// </remarks>
    public sealed class VirtualStream : IJson, IDisposable
    {
        private Stream stream;
        private string fileName;
        private string contentType;

        /// <summary>
        /// 使用目标流、文件名称和文件类型初始化 <see cref="VirtualStream"/> 的新实例。
        /// </summary>
        /// <param name="stream">目标流，必须支持读取操作，可计算长度。在发送给客户端后，流被自动关闭释放。</param>
        /// <param name="fileName">重命名的文件名称，如果包含扩展名称，则使用此扩展名称推测文件类型。</param>
        /// <param name="contentType">文件的 MIME 类型，如果没有提供，则根据文件的扩展名称自动推测。</param>
        /// <exception cref="ArgumentNullException">stream 为 null。</exception>
        /// <exception cref="ArgumentException">stream 不支持读取操作。</exception>
        public VirtualStream(Stream stream, string fileName = null, string contentType = null)
        {
            if (stream == null)
            {
                throw new ArgumentNullException("stream");
            }

            if (!stream.CanRead)
            {
                throw new ArgumentException("stream 不支持读取操作。", "stream");
            }
            
            this.fileName = fileName ?? "";
            this.contentType = string.IsNullOrEmpty(contentType) ? Mime.Of(Path.GetExtension(fileName)) : contentType;
            this.stream = stream;
        }

        /// <summary>
        /// 获取目标数据流。
        /// </summary>
        public Stream Stream
        {
            get
            {
                return this.stream;
            }
        }
        
        /// <summary>
        /// 获取文件名称。
        /// </summary>
        public string FileName
        {
            get
            {
                return this.fileName;
            }
        }

        /// <summary>
        /// 获取文件的类型。
        /// </summary>
        public string Type
        {
            get
            {
                return this.contentType;
            }
        }

        /// <summary>
        /// 将当前对象作为 JSON 字符串写入指定的文本流。
        /// </summary>
        /// <param name="writer">目标文本流。</param>
        void IJson.Write(TextWriter writer)
        {
            writer.Write("{{\"fileName\":\"{0}\",\"contentType\":\"{1}\"}}",
                Json.Escape(this.fileName), Json.Escape(this.contentType));
        }

        /// <summary>
        /// 返回文件名称的字符串。
        /// </summary>
        /// <returns>文件名称的字符串。</returns>
        public override string ToString()
        {
            return this.fileName;
        }

        /// <summary>
        /// 异步通过 WebSocket 发送文件。发送完毕后关闭流。
        /// </summary>
        /// <param name="progress">汇报异步操作的进度。</param>
        /// <param name="bufferSize">发送缓冲区的尺寸。</param>
        /// <returns>总共发送的字节数。</returns>
        /// <exception cref="ObjectDisposedException">目标流已经被关闭释放。</exception>
        internal async Task<long> SendAsync(WebSocketAsyncProgress progress, int bufferSize = 0)
        {
            if (this.stream == null)
            {
                throw new ObjectDisposedException("流已经被关闭。");
            }

            // 已经发送的字节数
            var sent = 0L;

            using (this.stream)
            {
                // 缓冲区尺寸，如果没有提供参数 bufferSize，则使用配置文件中的缓冲区尺寸
                bufferSize = bufferSize <= 0 ? WebSocketHelper.MaxBufferSize : bufferSize;

                // 发送数据的缓冲区
                var buffer = new byte[bufferSize];

                // 流的总长度
                var total = stream.Length;
                // 从目标流中当前读取的字节数
                var read = 0;

                while (sent < total)
                {
                    // 从流中异步读取数据
                    read = await stream.ReadAsync(buffer, 0, bufferSize, progress.Token);
                    if (read <= 0)
                    {
                        // 没有读取到任何数据
                        break;
                    }

                    // 增加读取计数
                    sent += read;

                    // 发送刚刚读取到的数据
                    await progress.Socket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Binary, sent >= total, progress.Token);
                }
                
            }

            // 关闭流
            this.stream.Close();
            this.stream = null;

            return sent;
        }

        /// <summary>
        /// 将文件作为响应内容附加到指定的响应消息，附加后不会关闭基础流，因为接下来实际响应时还会继续使用基础流。用于 AJAX 请求时下载文件。
        /// </summary>
        /// <param name="response">目标响应对象。</param>
        /// <exception cref="ObjectDisposedException">目标流已经被关闭释放。</exception>
        internal void ToResponseContent(HttpResponseMessage response)
        {
            if (this.stream == null)
            {
                throw new ObjectDisposedException("流已经被关闭。");
            }

            long? length = null;

            // 尝试获取流的长度
            try
            {
                length = this.stream.Length;
            }
            catch
            {
            }

            // 文件内容
            response.Content.Headers.ContentLength = length;
            response.Content = new StreamContent(this.stream);
            response.Content.Headers.ContentType = new MediaTypeHeaderValue(this.contentType);

            if (!string.IsNullOrEmpty(fileName))
            {
                // 文件名称
                response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
                {
                    FileName = this.fileName
                };
            }
        }

        /// <summary>
        /// 关闭基础流，释放当前流占用的资源。
        /// </summary>
        public void Dispose()
        {
            if (this.stream != null)
            {
                this.stream.Dispose();
                this.stream = null;
            }
        }
    }
}
